module Pjs2 {
    export class Pjs2Console extends HtmlConsole {
        parser = new Pjs2.Parser();
        history = new Array<string>();
        historyPos = 0;
        multiLine = false;
        script: string = "";
        runtime = new Runtime();
        showParsedScript: boolean = true;
        running: boolean = true;
        actualTableHead: string[] = null;
        catchErrors = true;

        constructor(container: HTMLDivElement) {
            super(container);

            this.runtime.AddFunction(new Pjs2.FunctionDefinition(
                (r, p) => {
                    this.showParsedScript = p["Value"];
                }, [
                    new FuncParameterDefinition("Value", true, true, o => typeof o == "boolean")
                ], "Set-ShowParsedScript"));
            this.runtime.AddAlias("showparsed", "Set-ShowParsedScript");

            this.runtime.AddFunction(new FunctionDefinition(
                (r, p) => {
                    this.WriteLine(p["object"].toString());
                    this.WriteLine("");
                }, [
                    new FuncParameterDefinition("Object", true, true)
                ], "Write-Host"));

            this.runtime.AddFunction(new FunctionDefinition(
                (r, p) => {
                    var obj = <Array<any>>p["inputobject"];
                    if (!(obj instanceof Array)) obj = [obj];

                    if (p["expand"]) obj = obj.map(o => o[p["expand"]]);

                    var par = new Array<string>();
                    if (p["property"]) {
                        var prop = <Array<any>>p["property"]
                        if (!(prop instanceof Array)) prop = [prop];
                        par = prop;
                    }
                    else {
                        var f = FormatingRules.FromObjTable(obj);
                        for (var p in f) {
                            par.push(p);
                        }
                    }

                    this.WriteTable(obj, p);
                }, [
                    new FuncParameterDefinition("Property", false, false),
                    new FuncParameterDefinition("InputObject", true, true),
                    new FuncParameterDefinition("AutoSize", false, false, o => typeof o == "boolean"),
                    new FuncParameterDefinition("Expand", false, false, o => typeof o == "string")
                ], "Format-Table"));

            this.runtime.AddFunction(new FunctionDefinition(
                (r, p) => {
                    var obj = <Array<any>>p["inputobject"];
                    if (!(obj instanceof Array)) obj = [obj];
                    if (p["expand"]) obj = obj.map(o => o[p["expand"]]);
                    obj.forEach(o => this.WriteJson(o, null, p["spacing"]));
                }, [
                    new FuncParameterDefinition("InputObject", true, true),
                    new FuncParameterDefinition("Expand", false, false, o => typeof o == "string"),
                    new FuncParameterDefinition("Spacing", false, false, o => typeof o == "string")
                ], "Format-Json"));
            this.runtime.AddAlias("fj", "Format-Json");

            this.runtime.AddFunction(new FunctionDefinition(
                (r, p) => {
                    if (p["prompt"] != undefined) {
                        this.Write(p["prompt"]);
                    }
                    var callback = new ReturnCallback();
                    this.ReadLine(s => {
                        if (!p["invisible"]) {
                            this.WriteLine(s);
                        }
                        callback.Return(s)
                        return false;
                    });
                    return callback;
                }, [
                    new FuncParameterDefinition("Prompt", false, true),
                    new FuncParameterDefinition("Invisible", false, false, o => typeof o == "boolean")
                ], "Read-Host"));
        }

        KeyPress(ev: KeyboardEvent) {
            if (!this.running) {
                if (ev.keyCode == 13 && ev.shiftKey) {
                    this.script += this.input.value + "\n";
                }
                else if (ev.keyCode == 13) {
                    this.script += this.input.value;
                    this.input.value = "";
                    this.CallReaders(this.script);
                    this.script = "";
                }
                super.KeyPress(ev, false);
                return;
            }
            if (ev.keyCode == 38) {
                this.historyPos--;
                if (this.historyPos < 0) this.historyPos = 0;
                if (this.history[this.historyPos]) this.input.value = this.history[this.historyPos];
            }
            else if (ev.keyCode == 40) {
                this.historyPos++;
                if (this.historyPos >= this.history.length) this.historyPos = this.history.length - 1;
                if (this.history[this.historyPos]) this.input.value = this.history[this.historyPos];
            }
            else if (ev.keyCode == 13 && ev.shiftKey) {
                this.multiLine = true;
                this.ReadScriptLine();
                this.Write("...> ");
            }
            else if (ev.keyCode == 13 && !ev.shiftKey) {
                this.input.value = this.input.value.trim();
                var lch = this.input.value.charAt(this.input.value.length - 1);
                if (lch == "|" || lch == "{" || lch == "(") {
                    this.multiLine = true;
                }
                var end = !(this.multiLine && this.input.value != "");
                this.ReadScriptLine();
                if (end) {
                    this.ExecuteScript();
                }
                else {
                    this.Write("...> ");
                }
            }
            super.KeyPress(ev, false);
            return true;
        }

        ReadScriptLine() {
            var val = this.input.value;
            this.input.value = "";
            this.script += val + "\n";
            this.WriteLine(val);
            this.history.push(val);
            this.historyPos = this.history.length;
        }

        ExecuteScript(): void {
            try {
                var p = this.parser.ReadScript(this.script);
            }
            catch (e) {
                this.WriteError("Parsing error: " + e);
                this.script = "";
                return;
            }
            this.script = "";
            if (this.showParsedScript) {
                this.settings.foregroundColor = "lightBlue";
                this.Write("Parsed script: ");
                this.WriteLine(JSON.stringify(p));
                this.settings.foregroundColor = "white";
            }

            this.running = false;

            try {
                var result = p.Run(this.runtime);
                Enumerable.forEach<any>(result, o => {
                    if (o != null)
                        this.WriteResult(o);
                }).OnError((e, f) => {
                        this.WriteError(e);
                        if (f) {
                            this.running = true;
                            this.Write("Pjs2> ");
                        }
                    }).OnMessage(msg => {
                        this.WriteLine(msg);
                    }).OnReturn(() => {
                        this.running = true;
                        this.Write("Pjs2> ");
                    });
            }
            catch (e) {
                this.running = true;
                this.WriteError("Runtime error: " + e);
                this.Write("Pjs2> ");
            }
        }

        WriteResult(data: any, actualTable: string[]= null) {
            var f = FormatingRules.FromObj(data);

            if (f.type == "table") {
                // TODO: tables
                this.WriteError("sorry table are not currently supported");
            }
            else if (f.type == "list") {
                this.WriteList(data, f);
            }
            else if (f.type == "json") {
                this.WriteJson(data, f);
            }
            else if (f.type == "toString") {
                this.WriteLine(data.toString());
            }
            else if (f.type == "custom") {
                f.customFormater(data, this);
            }
            else throw "not supported format";
        }

        WriteFormatedData(data: any[], format: FormatingRules) {
            var fn = format.type;
            if (fn != "list" && fn != "table" && fn != "json" && fn != "toString") throw "invalid format identifier";
            if (fn == "table") {
                return this.WriteTable(data, format.parameterNames);
            }
            if (fn == "toString") return this.WriteLine(data.toString());
        }

        WriteResultItem(data: any, format: FormatingRules) {
            if (data instanceof Array) this.WriteFormatedData(data, format);
            else if (format.type == "json")
                this.WriteJson(data, format);
            else if (format.type == "list") {
                this.WriteList(data, format);
            }
        }

        WriteList(data: any, format: FormatingRules = null) {
            var props = new Array<string>();
            for (var p in data) {
                if (typeof data[p] != "function") {
                    props.push(p);
                }
            }
            var l = 0;
            props.forEach(v => {
                if (l < v.length) l = v.length;
            });
            props.forEach(v => {
                this.Write(v);
                this.Write(Array(l + 2 - v.length).join(" "));
                this.Write(": ");
                this.WriteLine(Pjs2Console.ObjToString(data[v]));
            });
        }

        WriteTable(data: any[], params: string[], autoSize: boolean = true) {
            var t = new Array<Array<string>>();
            var colWidth = new Array<number>();
            for (var i = 0; i < params.length; i++) {
                colWidth[i] = params[i].length;
            }
            if (!autoSize) {
                var w = this.width;
                colWidth = Array<number>(params.length).map(cw => w / params.length);
            }
            data.forEach(d => {
                var row = new Array<string>();
                params.forEach((p, i) => {
                    var cell = Pjs2Console.ObjToString(d[p])
                    row.push(cell);
                    if (autoSize && colWidth[i] < cell.length) colWidth[i] = cell.length;
                });
                t.push(row);
            });

            this.WriteTableRow(params, colWidth);
            this.WriteTableRow(params.map(p => Array(p.length + 1).join("-")), colWidth);
            t.forEach(r => {
                this.WriteTableRow(r, colWidth);
            });
            this.WriteLine("");
        }

        private WriteTableRow(data: string[], w: number[]) {
            data.forEach((d, i) => {
                this.Write(d);
                this.Write(Array(w[i] - d.length + 2).join(" "));
            });
            this.WriteLine("");
        }

        WriteJson(data, format: FormatingRules = null, spacing = "\t") {
            if (!format || !format.params) this.WriteLine(JSON.stringify(data.value, null, spacing));
            else {
                var obj = {};
                for (var p in format.params) {
                    obj[p] = data.value[p];
                }
                this.WriteLine(JSON.stringify(obj, null, spacing));
            }
        }

        WriteError(msg: string) {
            var f = this.settings.backgroundColor;
            this.settings.backgroundColor = "red";
            this.WriteLine(msg);
            this.settings.backgroundColor = f;
        }

        static ObjToString(data: any): string {
            if (data == null) return "null";
            if (data == undefined) return "undefined";
            if (data instanceof Array) return "[" + data.length + "]";

            var t = data.$type || typeof data;
            if (t == "function") return "function (...) {...}";
            if (FormatingRules.FromObj(data).type == "toString") return data.toString();
            return t + "{...}";
        }
    }

    export class FormatingRules {
        customFormater: (obj: any, cons: Pjs2Console) => void;
        get parameterNames(): string[] {
            var p = new Array<string>();
            for (var i in this.params) { p.push(i); }
            return p;
        }
        constructor(public type: string, public params: { [s: string]: FormatingRules } = null) { }

        static RulesForTypes: { [s: string]: (obj: any) => FormatingRules } = {
            "number": (obj) => new FormatingRules("toString"),
            "string": o => new FormatingRules("toString"),
            "boolean": o => new FormatingRules("toString"),
            "function": o => new FormatingRules("toString"),
            "object": o => new FormatingRules("json"),
            "FunctionInfo": (obj) => new FormatingRules("table", { name: new FormatingRules("toString"), type: new FormatingRules("toString") })
        };

        static FromObj(obj): FormatingRules {
            if (typeof obj.$getFromattingRules === "function") {
                return obj.$getFromattingRules();
            }
            if (obj.$formattingRules != null) {
                return obj.$formattingRules;
            }
            if (obj.$type != null && FormatingRules.RulesForTypes[obj.$type]) {
                return FormatingRules.RulesForTypes[obj.$type](obj);
            }
            return this.RulesForTypes[typeof obj](obj);
        }

        static FromObjTable(obj: any[]): FormatingRules {
            return FormatingRules.Table(FormatingRules.IntersectParameters(obj));
        }

        static Table(p: string[]): FormatingRules {
            var params: { [s: string]: FormatingRules } = {};
            p.forEach(v => params[v] = new FormatingRules("toString"));
            return new FormatingRules("table", params);
        }

        static IntersectParameters(obj: any[]): string[] {
            var p = FormatingRules.FromObj(obj[0]).parameterNames;

            obj.forEach(v => {
                var lp = FormatingRules.FromObj(v).parameterNames;
                p = p.filter(i => lp.some(lpi => lpi == i));
            });
            return p;
        }
    }
}